
目前我们只对最终图像应用色调映射,将HDR颜色转换到LDR进行显示,但这不是调整图像颜色的唯一方法。视频、照片和数字图像的颜色调整大致有三步,首先是颜色校正,目的是使图像与观察场景时的图像匹配;第二步是颜色分级,对最终图像进行颜色和亮度的改变或矫正,可以理解为增加滤镜,这两步通常合并成一个颜色分级(Color Grading)步骤;最后一步则执行色调映射,将HDR颜色转换到LDR进行显示。只有色调映射的图像往往不那么丰富多彩,除非它非常明亮,ACES可以稍微增加深色的对比度,但它不能替代颜色分级,本节将以Neutral(中性)色调映射为基础。
12.1.1 颜色分级(Color Grading)
1. 我们在执行色调映射之前进行颜色分级,在PostFXStackPasses.hlsl中定义一个ColorGrade方法,最初只是将颜色值限制到60以下。
//颜色分级
float3 ColorGrade (float3 color)
{
color = min(color, 60.0);
return color;
}
2. 在各个色调映射模式中使用该方法进行颜色分级,并添加一个新的Tone Mapping None Pass和ToneMappingNonePassFragment片元函数,只进行颜色分级,不进行色调映射。
float4 ToneMappingNonePassFragment (Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb = ColorGrade(color.rgb);
return color;
}
float4 ToneMappingReinhardPassFragment(Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb = ColorGrade(color.rgb);
color.rgb /= color.rgb + 1.0;
return color;
}
float4 ToneMappingNeutralPassFragment(Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb = ColorGrade(color.rgb);
color.rgb = NeutralTonemap(color.rgb);
return color;
}
float4 ToneMappingACESPassFragment(Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb = ColorGrade(color.rgb);
color.rgb = AcesTonemap(unity_to_ACES(color.rgb));
return color;
}
Pass
{
Name "Tone Mapping None"
HLSLPROGRAM
#pragma target 3.5
#pragma vertex DefaultPassVertex
#pragma fragment ToneMappingNonePassFragment
ENDHLSL
}
3. 调整ToneMappingSettings中的Mode枚举,枚举值从0开始。
public enum Mode
{
None,
ACES,
Neutral,
Reinhard
}
4. 调整DoToneMapping方法,没有色调映射时使用ToneMappingNone Pass而不是Copy Pass。
void DoToneMapping(int sourceId)
{
PostFXSettings.ToneMappingSettings.Mode mode = settings.ToneMapping.mode;
Pass pass =Pass.ToneMappingNone + (int)mode;
Draw(sourceId, BuiltinRenderTextureType.CameraTarget, pass);
}
12.1.2 设置
1. 我们将复制URP和HDRP的颜色调整后处理工具的功能。这一步是将它的相关配置结构添加到PostFXSettings脚本中,URP和HDRP的颜色分级功能相同,我们将以相同的顺序添加相同的颜色分级配置选项。首先是后曝光,用于调整场景的整体曝光度,这是一个不受限制的浮点数;第二个是对比度,用来扩大或缩小色调值的总体范围,限制在[-100,100]之间;第三个是颜色滤镜,通过乘以颜色来给渲染器着色,这是一个没有Alpha的HDR颜色;第四个是色调偏移,用来改变所有颜色的色调,限制在[-180°,+180°]之间;最后一个是饱和度,用来推动所有颜色的强度,限制在[-100,100]之间。这些选项的初始默认值为零,颜色滤波为白色,这些设置不会改变图像。
[Serializable]
public struct ColorAdjustmentsSettings
{
//后曝光,调整场景的整体曝光度
public float postExposure;
//对比度,扩大或缩小色调值的总体范围
[Range(-100f, 100f)]
public float contrast;
//颜色滤镜,通过乘以颜色来给渲染器着色
[ColorUsage(false, true)]
public Color colorFilter;
//色调偏移,改变所有颜色的色调
[Range(-180f, 180f)]
public float hueShift;
//饱和度,推动所有颜色的强度
[Range(-100f, 100f)]
public float saturation;
}
[SerializeField]
ColorAdjustmentsSettings colorAdjustments = new ColorAdjustmentsSettings
{
colorFilter = Color.white
};
public ColorAdjustmentsSettings ColorAdjustments => colorAdjustments;

2. 我们将同时进行颜色分级和色调映射,因此将DoToneMapping方法重命名为DoColorGradingAndToneMapping,另外新建ConfigureColorAdjustments方法获取颜色调整的配置并在DoColorGradingAndToneMapping方法的最开始调用它。
using static PostFXSettings;
//获取颜色调整的配置
void ConfigureColorAdjustments()
{
ColorAdjustmentsSettings colorAdjustments = settings.ColorAdjustments;
}
void DoColorGradingAndToneMapping(int sourceId)
{
ConfigureColorAdjustments();
ToneMappingSettings.Mode mode = settings.ToneMapping.mode;
...
}
3. 我们要在着色器中进行颜色分级,所以需要把颜色调整的配置发送到GPU,定义颜色调整和颜色滤镜的着色器标识ID,将后曝光、对比度、色调偏移和饱和度封装到一个Vector4矢量中,其中曝光度需要进行2的幂次方,对比度和饱和度转换到[0,2]范围内,色调偏移转换到[-0.5,0.5]范围内,且颜色滤镜必须位于线性色彩空间中。
int colorAdjustmentsId = Shader.PropertyToID("_ColorAdjustments");
int colorFilterId = Shader.PropertyToID("_ColorFilter");
void ConfigureColorAdjustments()
{
ColorAdjustmentsSettings colorAdjustments = settings.ColorAdjustments;
buffer.SetGlobalVector(colorAdjustmentsId, new Vector4(
Mathf.Pow(2f, colorAdjustments.postExposure),
colorAdjustments.contrast * 0.01f + 1f,
colorAdjustments.hueShift * (1f / 360f),
colorAdjustments.saturation * 0.01f + 1f));
buffer.SetGlobalColor(colorFilterId, colorAdjustments.colorFilter.linear);
}
12.1.3 后曝光
在着色器侧声明颜色调整矢量和颜色滤镜属性,我们将所有的颜色分级操作都封装到单独的方法中。首先是后曝光,创建一个ColorGradePostExposure方法将颜色值和后曝光属性相乘,然后在ColorGrade中调用。
float4 _ColorAdjustments;
float4 _ColorFilter;
float3 ColorGradePostExposure (float3 color)
{
return color * _ColorAdjustments.x;
}
//颜色分级
float3 ColorGrade (float3 color)
{
color = min(color, 60.0);
color = ColorGradePostExposure(color);
return color;
}
下图是后曝光数值调节到-2和2时的画面效果。后曝光的原理是,它模仿相机的曝光,通常是在所有其它后处理特效渲染之后,且在其它颜色分级步骤之前应用。它是一种非现实主义的艺术工具,可用于调整场景的整体曝光度,而不影响其它效果(如Bloom)。


12.1.4 对比度
第二个颜色调整是对比度,通过颜色从中减去均匀中灰色,然后按对比度缩放,添加中灰色来应用它,使用ACEScc_MIDGRAY作为灰色,其中ACEScc是ACES颜色空间的对数子集,中灰色值为0.4135884。我们在PostFXStackPasses.hlsl中定义ColorGradingContrast方法。为了获得最好的效果,此计算是在Log C空间中完成的,而不是线性色彩空间。我们使用源码库文件的LinearToLogC方法将颜色从线性空间转换到Log C空间,计算完成后再调用LogCToLinear方法将颜色值转回线性空间。最后在进行后曝光之后增加对比度。
float3 ColorGradingContrast (float3 color)
{
color = LinearToLogC(color);
color = (color - ACEScc_MIDGRAY) * _ColorAdjustments.y + ACEScc_MIDGRAY;
return LogCToLinear(color);
}
//颜色分级
float3 ColorGrade (float3 color)
{
color = min(color, 60.0);
color = ColorGradePostExposure(color);
color = ColorGradingContrast(color);
return color;
}

当对比度增加时,可能导致颜色分量为负值,这可能会打乱后续的颜色调整。因此调整对比度后需要消除负值。
float3 ColorGrade (float3 color)
{
...
color = ColorGradingContrast(color);
//消除负值
color = max(color, 0.0);
return color;
}
下面是对比度为-50和50的效果。


12.1.5 颜色滤镜
接下来是颜色滤镜,只需将其与颜色相乘即可,它适用于负值,所以可以在消除负值之前调用。
float3 ColorGradeColorFilter (float3 color)
{
return color * _ColorFilter.rgb;
}
//颜色分级
float3 ColorGrade (float3 color)
{
...
color = ColorGradeColorFilter(color);
//消除负值
color = max(color, 0.0);
return color;
}
下图是颜色滤镜调为青色的效果, 消除了大多数红色光。

12.1.6 色调偏移

色调偏移可以改变颜色色调,URP和HDRP在颜色滤镜后执行色调偏移,我们将使用相同的颜色调整顺序。颜色的色调通过RgbToHsv方法将颜色格式从RGB转换为HSV,将色调偏移添加到H,并通过HsvToRgb转换回来。因为色调是在0~1的颜色滚轮上定义的,如果颜色超出范围则使用RotateHue方法进行限制。调整后的色调将0和1作为参数,必须在消除负值后进行色调偏移。
float3 ColorGradingHueShift (float3 color)
{
color = RgbToHsv(color);
float hue = color.x + _ColorAdjustments.z;
color.x = RotateHue(hue, 0.0, 1.0);
return HsvToRgb(color);
}
//颜色分级
float3 ColorGrade (float3 color)
{
...
color = max(color, 0.0);
color = ColorGradingHueShift(color);
return color;
}
下面是180°的色调偏移效果。

12.1.7 饱和度
最后我们调整饱和度,首先通过Luminance方法获得颜色的亮度,然后计算方式跟对比度一样,只不过使用颜色亮度而不是中间灰度值参与计算,也不用转到Log C空间。因为这可能再次产生负值,所以调整饱和度后我们需要再一次消除负值。
float3 ColorGradingSaturation (float3 color)
{
float luminance = Luminance(color);
return (color - luminance) * _ColorAdjustments.w + luminance;
}
float3 ColorGrade (float3 color)
{
...
color = ColorGradingHueShift(color);
color = ColorGradingSaturation(color);
return max(color, 0.0);
return color;
}
下图是饱和度为-100和100时的效果。


颜色调整工具并不是URP和HDRP提供的唯一颜色分级选项。我们将再增加一些支持。
12.2.1 白平衡(White Balance)
白平衡工具使调整图像的感知温度成为可能。它有两个范围在[−100,100]的属性调节滑块。第一个属性是色温,这里的参数以0为基准,可以相应地调整白平衡的冷暖偏向,小于0则色调变冷,大于0色调变暖。第二个属性是色调,用于调整温度变化后的颜色,如果说色温的色彩偏向是黄、蓝,那么这个参数的色彩偏向绿和品红,小于0时色调偏绿,大于0时色调偏品红。这里将白平衡的配置结构添加到PostFXSettings.cs中。
[Serializable]
public struct WhiteBalanceSettings
{
//色温,调整白平衡的冷暖偏向
[Range(-100f, 100f)]
public float temperature;
//色调,调整温度变化后的颜色
[Range(-100f, 100f)]
public float tint;
}
[SerializeField]
WhiteBalanceSettings whiteBalance = default;
public WhiteBalanceSettings WhiteBalance => whiteBalance;

1. 声明一个用于调节白平衡的着色器标识ID,定义一个ConfigureWhiteBalance方法,通过调用源码库中的ColorUtils.ColorBalanceToLMSCoeffs方法并传递色温和色调得到白平衡颜色并传递到GPU,然后在DoColorGradingAndToneMapping方法中调整颜色后调节白平衡。
int whiteBalanceId = Shader.PropertyToID("_WhiteBalance");
void ConfigureWhiteBalance()
{
WhiteBalanceSettings whiteBalance = settings.WhiteBalance;
buffer.SetGlobalVector(whiteBalanceId, ColorUtils.ColorBalanceToLMSCoeffs(whiteBalance.temperature, whiteBalance.tint));
}
void DoColorGradingAndToneMapping(int sourceId)
{
ConfigureColorAdjustments();
ConfigureWhiteBalance();
...
}
2. 在PostFXStackPasses.hlsl中声明白平衡颜色数据,定义ColorGradeWhiteBalance方法来应用白平衡。首先从线性色彩空间转到LMS色彩空间,然后乘以白平衡颜色后转回线性空间。LMS是由人眼的三种锥体的响应表示的颜色空间,以其在长波长、中波长和短波长处的响应度(灵敏度)命名,在执行色适应时估计样本在不同光源下的外观时,通常使用LMS色彩空间。当一个或多个锥体有缺陷时,它也可用于色盲的研究。最后在后曝光和对比度之间应用白平衡。
float4 _WhiteBalance;
float3 ColorGradeWhiteBalance (float3 color)
{
color = LinearToLMS(color);
color *= _WhiteBalance.rgb;
return LMSToLinear(color);
}
float3 ColorGrade (float3 color)
{
color = min(color, 60.0);
color = ColorGradePostExposure(color);
color = ColorGradeWhiteBalance(color);
color = ColorGradingContrast(color);
...
}
下面是将色温设置为-100和100的效果,低温使图像变蓝,高温使图像变黄。


下面是色调调为-100和100时的效果,色调用于调整颜色平衡,色彩偏向绿色或品红。


12.2.2 色调分离(Split Toning)
色调分离工具可以根据亮度值对图像的不同区域着色,可以用它给场景的阴影和高光添加不同的色调,一个典型的例子是将阴影色调调成蓝色,将高光的色调调成暖橙色。
1. 在PostFXSettings.cs中创建色调分离的配置结构,它需要2个不含Alpha通道的LDR颜色,用于对阴影和高光着色,它们的默认值是灰色。还有一个范围在[-100,100]的滑块属性,使用该滑块来设置阴影和高光之间的平衡。较低的值会导致阴影色调比高光色调更明显。较高的值会产生相反的效果,与阴影色调相比,高光色调更明显。
[Serializable]
public struct SplitToningSettings
{
//用于对阴影和高光着色
[ColorUsage(false)]
public Color shadows, highlights;
//设置阴影和高光之间的平衡的滑块
[Range(-100f, 100f)]
public float balance;
}
[SerializeField]
SplitToningSettings splitToning = new SplitToningSettings
{
shadows = Color.gray,
highlights = Color.gray
};
public SplitToningSettings SplitToning => splitToning;

2. 在PostFXStack.cs中声明阴影和高光色调的着色器标识ID,定义ConfigureSplitToning方法将阴影和高光颜色传到GPU,颜色保持在伽马颜色空间内,将平衡值缩放到[-1,1]的范围,并将其存储在阴影颜色的A通道中。
int splitToningShadowsId = Shader.PropertyToID("_SplitToningShadows");
int splitToningHighlightsId = Shader.PropertyToID("_SplitToningHighlights");
void ConfigureSplitToning()
{
SplitToningSettings splitToning = settings.SplitToning;
Color splitColor = splitToning.shadows;
splitColor.a = splitToning.balance * 0.01f;
buffer.SetGlobalColor(splitToningShadowsId, splitColor);
buffer.SetGlobalColor(splitToningHighlightsId, splitToning.highlights);
}
void DoColorGradingAndToneMapping(int sourceId)
{
ConfigureColorAdjustments();
ConfigureWhiteBalance();
ConfigureSplitToning();
...
}
3. 在PostFXStackPasses.hlsl中声明阴影和高光颜色属性,并定义ColorGradeSplitToning方法,在近似的伽马空间中分离色调,先将颜色值提高到1/2.2,然后再提高到2.2,这样做是为了匹配Adobe产品的色调分离。我们在颜色滤镜之后,色调偏移之前进行色调分离。
float4 _SplitToningShadows;
float4 _SplitToningHighlights;
//色调分离
float3 ColorGradeSplitToning (float3 color)
{
color = PositivePow(color, 1.0 / 2.2);
return PositivePow(color, 2.2);
}
float3 ColorGrade (float3 color)
{
...
color = ColorGradeSplitToning(color);
color = ColorGradingHueShift(color);
...
}
4. 我们首先将色调限制在各自区域,得到平衡值,然后通过Lerp方法在中性值0.5和它们自身的颜色之间进行插值,然后我们通过颜色和阴影色调之间使用SoftLight方法进行柔光混合,最后再和高光色调进行柔光混合来应用色调。
float3 ColorGradeSplitToning (float3 color)
{
color = PositivePow(color, 1.0 / 2.2);
float t = saturate(Luminance(saturate(color)) + _SplitToningShadows.w);
float3 shadows = lerp(0.5, _SplitToningShadows.rgb, 1.0 - t);
float3 highlights = lerp(0.5, _SplitToningHighlights.rgb, t);
color = SoftLight(color, shadows);
color = SoftLight(color, highlights);
return PositivePow(color, 2.2);
}
下面是将阴影的色调设置为蓝色,高光设置为橙色的效果。


12.2.3 通道混合器(Channel Mixer)
支持的另一个工具是通道混合器,它允许你组合输入RGB值以创建新的RGB值。例如你可以交换R和G,从G中减去B,或将G添加到R中,将绿色推向黄色。通俗来说通道混合器修改每个输入颜色通道对输出通道的总体混合的影响。例如增加绿色通道受到红色通道的整体混合的影响,最终图像的所有区域的绿色(包括中性/单色)色调会变成更红的色调。混合器本质上是一个3*3转换矩阵,默认矩阵是单位矩阵,我们可以使用3个Vector3类型的值,用于红色、绿色和蓝色配置。
1. 在PostFXSettings.cs中定义通道混合器的配置结构。
[Serializable]
public struct ChannelMixerSettings
{
public Vector3 red, green, blue;
}
[SerializeField]
ChannelMixerSettings channelMixer = new ChannelMixerSettings
{
red = Vector3.right,
green = Vector3.up,
blue = Vector3.forward
};
public ChannelMixerSettings ChannelMixer => channelMixer;

2. 在PostFXStack.cs中声明这三个向量的着色器标识ID,并定义ConfigureChannelMixer方法将数据发送到GPU。
int channelMixerRedId = Shader.PropertyToID("_ChannelMixerRed");
int channelMixerGreenId = Shader.PropertyToID("_ChannelMixerGreen");
int channelMixerBlueId = Shader.PropertyToID("_ChannelMixerBlue");
void ConfigureChannelMixer()
{
ChannelMixerSettings channelMixer = settings.ChannelMixer;
buffer.SetGlobalVector(channelMixerRedId, channelMixer.red);
buffer.SetGlobalVector(channelMixerGreenId, channelMixer.green);
buffer.SetGlobalVector(channelMixerBlueId, channelMixer.blue);
}
void DoColorGradingAndToneMapping(int sourceId)
{
ConfigureColorAdjustments();
ConfigureWhiteBalance();
ConfigureSplitToning();
ConfigureChannelMixer();
...
}
3. 定义ColorGradingChannelMixer方法对颜色值进行矩阵的乘法,在色调分离后执行该步骤。然后,我们需要再次消除负值,因为可能产生负的颜色通道值。
float4 _ChannelMixerRed;
float4 _ChannelMixerGreen;
float4 _ChannelMixerBlue;
float3 ColorGradingChannelMixer(float3 color)
{
return mul(float3x3(_ChannelMixerRed.rgb, _ChannelMixerGreen.rgb, _ChannelMixerBlue.rgb),color);
}
float3 ColorGrade (float3 color)
{
...
color = ColorGradeSplitToning(color);
color = ColorGradingChannelMixer(color);
color = max(color, 0.0);
...
}
下图是绿色在GB之间分离,蓝色在RGB之间分离的效果。

12.2.4 Shadows Midtones Highlights
我们将支持的最后一个工具是Shadows Midtones Highlights。它的工作原理类似于色调分离,不同之处在于它还可以控制中间色调,可以分别控制阴影,中间色调和高光区域。
1. 在PostFXSettings中,我们定义相关配置结构。Unity组件使用了色盘和可视化的区域权重来进行调节,我们则使用3个LDR色调和4个滑块来控制阴影、中间色调和高光。我们不使用色盘的原因是,Unity 没有包含在编辑器中的默认色轮编辑Widget组件,但URP和HDRP中都是有的,区域的GUI也是自定义的。滑块分别用于设置阴影和渲染中间色调之间过渡的起始点和结束点、渲染中间色调和高光之间过渡的起始点和结束点。阴影强度从起始到结束逐渐下降,高光强度从起始到结束逐渐增加。这些滑块使用[0,2]的范围以便可以进入一点HDR范围,颜色默认为白色,然后使用和Unity相同的区域默认值,即阴影区域为0-0.3,高光区域为0.55-1。
[Serializable]
public struct ShadowsMidtonesHighlightsSettings
{
[ColorUsage(false, true)]
public Color shadows, midtones, highlights;
[Range(0f, 2f)]
public float shadowsStart, shadowsEnd, highlightsStart, highLightsEnd;
}
[SerializeField]
ShadowsMidtonesHighlightsSettings
shadowsMidtonesHighlights = new ShadowsMidtonesHighlightsSettings
{
shadows = Color.white,
midtones = Color.white,
highlights = Color.white,
shadowsEnd = 0.3f,
highlightsStart = 0.55f,
highLightsEnd = 1f
};
public ShadowsMidtonesHighlightsSettings ShadowsMidtonesHighlights => shadowsMidtonesHighlights;

2. 在PostFXStack.cs中定义ConfigureShadowsMidtonesHighlights方法,将三种颜色转换到线性色彩空间,最后和区域范围一起发送给GPU。
int smhShadowsId = Shader.PropertyToID("_SMHShadows");
int smhMidtonesId = Shader.PropertyToID("_SMHMidtones");
int smhHighlightsId = Shader.PropertyToID("_SMHHighlights");
int smhRangeId = Shader.PropertyToID("_SMHRange");
void ConfigureShadowsMidtonesHighlights()
{
ShadowsMidtonesHighlightsSettings smh = settings.ShadowsMidtonesHighlights;
buffer.SetGlobalColor(smhShadowsId, smh.shadows.linear);
buffer.SetGlobalColor(smhMidtonesId, smh.midtones.linear);
buffer.SetGlobalColor(smhHighlightsId, smh.highlights.linear);
buffer.SetGlobalVector(smhRangeId, new Vector4(smh.shadowsStart, smh.shadowsEnd, smh.highlightsStart, smh.highLightsEnd));
}
void DoColorGradingAndToneMapping(int sourceId)
{
...
ConfigureChannelMixer();
ConfigureShadowsMidtonesHighlights();
...
}
3. 在着色器中我们定义ColorGradingShadowsMidtonesHighlights方法,使用颜色值分别乘以这三种颜色,并且每种颜色都按自身的权重进行缩放,然后对结果求和。权重是基于亮度的,阴影权重从1开始,使用smoothstep方法从起始点到结束点逐渐降至0,而高光权重从0逐渐增加到1,中间色调权重等于1减去它们两个的权重,想法是阴影和高光区域不会重叠或只重叠一点,所以中间色调权重永远不会变为负数。
float4 _SMHShadows;
float4 _SMHMidtones;
float4 _SMHHighlights;
float4 _SMHRange;
float3 ColorGradingShadowsMidtonesHighlights(float3 color)
{
float luminance = Luminance(color);
float shadowsWeight = 1.0 - smoothstep(_SMHRange.x, _SMHRange.y, luminance);
float highlightsWeight = smoothstep(_SMHRange.z, _SMHRange.w, luminance);
float midtonesWeight = 1.0 - shadowsWeight- highlightsWeight;
return color * _SMHShadows.rgb * shadowsWeight +
color * _SMHMidtones.rgb * midtonesWeight +
color * _SMHHighlights.rgb * highlightsWeight;
}
float3 ColorGrade (float3 color)
{
...
color = ColorGradingChannelMixer(color);
color = max(color, 0.0);
color = ColorGradingShadowsMidtonesHighlights(color);
...
下图是蓝色的阴影,粉红色的中间色调,黄色的高光的效果。

12.2.5 ACES色彩空间
使用ACES色调映射时,Unity在ACES色彩空间而不是线性色空间中执行大多数颜色分级来产生更好的结果,让我们也这样做。
1. 后曝光和白平衡始终应用于线性空间,应用对比度时开始转换到Log C空间进行计算。在ColorGradingContrast方法中添加一个bool参数useACES,如果使用了ACES,先从线性空间转换到ACES,然后转换到ACEScc色彩空间,而不是Log C空间。我们可以通过unity_to_ACES和ACES_to_ACEScc方法进行转换。调整完对比度后,通过ACEScc_to_ACES和ACES_to_ACEScg方法将颜色转换到ACEScg色彩空间并返回,而不是返回线性空间。ACEScg是ACES色彩空间的线性子集。
float3 ColorGradingContrast (float3 color, bool useACES)
{
color = useACES ? ACES_to_ACEScc(unity_to_ACES(color)) : LinearToLogC(color);
color = (color - ACEScc_MIDGRAY) * _ColorAdjustments.y + ACEScc_MIDGRAY;
return useACES ? ACES_to_ACEScg(ACEScc_to_ACES(color)) : LogCToLinear(color);
}
2. 从现在开始,在颜色分级对比度步骤后,会进入线性或ACEScg颜色空间。除了要在ACEScg颜色空间中使用AcesLuminance方法计算亮度外,其它操作仍相同。我们定义一个Luminance方法来计算亮度。
float Luminance(float3 color, bool useACES)
{
return useACES ? AcesLuminance(color) : Luminance(color);
}
3. ColorGradeSplitToning、ColorGradingShadowsMidtonesHighlights和ColorGradingSaturation方法都会使用亮度,我们替换成调用Luminance方法进行计算。
float3 ColorGradeSplitToning (float3 color, bool useACES)
{
color = PositivePow(color, 1.0 / 2.2);
float t = saturate(Luminance(saturate(color), useACES) + _SplitToningShadows.w);
...
}
float3 ColorGradingShadowsMidtonesHighlights(float3 color, bool useACES)
{
float luminance = Luminance(color, useACES);
...
}
float3 ColorGradingSaturation (float3 color, bool useACES)
{
float luminance = Luminance(color, useACES);
return (color - luminance) * _ColorAdjustments.w + luminance;
}
4. 该bool参数也要添加到ColorGrade方法中,默认为false,将其传递给需要使用它作为传参的方法中。如果该值为true,最终颜色应使用ACEScg_to_ACES方法将颜色转换到ACES色彩空间中。
float3 ColorGrade (float3 color, bool useACES = false)
{
color = min(color, 60.0);
color = ColorGradePostExposure(color);
color = ColorGradeWhiteBalance(color);
color = ColorGradingContrast(color, useACES);
color = ColorGradeColorFilter(color);
color = max(color, 0.0);
color = ColorGradeSplitToning(color, useACES);
color = ColorGradingChannelMixer(color);
color = max(color, 0.0);
color = ColorGradingShadowsMidtonesHighlights(color, useACES);
color = ColorGradingHueShift(color);
color = ColorGradingSaturation(color, useACES);
return max(useACES ? ACEScg_to_ACES(color) : color, 0.0);
return color;
}
5. 调整ToneMappingACESPassFragment片元函数的ColorGrade方法的调用传参,以表明它使用ACES。因为调整后的颜色将位于ACES色彩空间中,所以可以直接作为参数传递给ACESTonemap方法。
float4 ToneMappingACESPassFragment(Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb = ColorGrade(color.rgb, true);
color.rgb = AcesTonemap(color.rgb);
return color;
}
下面是我们使用ACES和Neutral色调映射的对比效果。


执行每个像素的所有颜色分级步骤是一个很大的工作量,我们虽然可以添加一些只执行某些步骤的变体,但也需要声明大量的关键字。相反我们可以将颜色分级烘焙到查找表中(简称LUT,Look-Up-Table),通过对其采样来转换颜色。LUT是一种3D纹理,通常为32*32*32,用烘焙填充该纹理并稍后进行采样比直接对整个图像执行颜色分级要少很多工作量,URP和HDRP也使用这个方法。
12.3.1 LUT分辨率
1. 通常颜色LUT分辨率为32就足够了,但我们将其作为可配置项,我们在CustomRenderPipelineAsset脚本中声明一个ColorLUTResolution枚举,默认分辨率为32,然后作为整数传递给渲染管线的构造函数。这里我们不能提供任意分辨率的配置项,是因为URP和HDRP允许的任意LUT分辨率最高为65,但当分辨率不为2的幂次方时,LUT采样会出错,URP也有这个问题。
public enum ColorLUTResolution
{
_16 = 16,
_32 = 32,
_64 = 64
}
//LUT分辨率
[SerializeField]
ColorLUTResolution colorLUTResolution = ColorLUTResolution._32;
//重写抽象方法,需要返回一个RenderPipeline实例对象
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline(allowHDR, useDynamicBatching, useGPUInstancing, useSRPBatcher, useLightsPerObject, shadows, postFXSettings, (int)colorLUTResolution);
}
2. 在CustomRenderPipeline脚本中跟踪LUT分辨率,并作为参数传递给CameraRenderer.Render方法。
int colorLUTResolution;
public CustomRenderPipeline(..., int colorLUTResolution)
{
this.colorLUTResolution = colorLUTResolution;
...
}
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
foreach (Camera camera in cameras)
{
renderer.Render(..., colorLUTResolution);
}
}
3. CameraRenderer.Render方法中又将其传递给postFXStack.Setup方法。
public void Render (...,int colorLUTResolution)
{
…
postFXStack.Setup(context, camera, postFXSettings, useHDR, colorLUTResolution);
…
}
4. 最后PostFXStack脚本也会跟踪该值。
int colorLUTResolution;
public void Setup(..., int colorLUTResolution)
{
this.colorLUTResolution = colorLUTResolution;
...
}
12.3.2 渲染到2D LUT纹理
1. LUT是3D纹理,但常规着色器无法渲染到3D纹理。因此我们将2D切片连续放置一排,组合成一个宽的2D纹理来模拟3D纹理。这样LUT纹理的高度等于配置的分辨率,其宽度等于分辨率的平方,然后使用默认HDR格式获取具有该大小的临时渲染纹理。在DoColorGradingAndToneMapping方法中配置颜色分级后进行此操作。
int colorGradingLUTId = Shader.PropertyToID("_ColorGradingLUT");
void DoColorGradingAndToneMapping(int sourceId)
{
...
ConfigureShadowsMidtonesHighlights();
int lutHeight = colorLUTResolution;
int lutWidth = lutHeight * lutHeight;
buffer.GetTemporaryRT(colorGradingLUTId, lutWidth, lutHeight, 0,FilterMode.Bilinear, RenderTextureFormat.DefaultHDR);
...
}
2. 从现在开始,我们要将颜色分级和色调映射都渲染到LUT纹理中,重命名现有的色调映射通道,因此ToneMappingNone将变为ColorGradingNone等等,然后将源纹理渲染到LUT纹理中而不是相机目标中,最后再将源纹理数据拷贝到相机目标,以获取未经调整的图像作为最终结果,并释放LUT。
enum Pass
{
...
Copy,
ColorGradingNone,
ColorGradingACES,
ColorGradingNeutral,
ColorGradingReinhard
}
void DoColorGradingAndToneMapping(int sourceId)
{
...
ToneMappingSettings.Mode mode = settings.ToneMapping.mode;
Pass pass = Pass.ColorGradingNone + (int)mode;
Draw(sourceId, colorGradingLUTId, pass);
Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Copy);
buffer.ReleaseTemporaryRT(colorGradingLUTId);
}
现在我们绕过了颜色分级和色调映射,但帧调试器显示我们在最终拷贝之前绘制了扁平化的图像。

12.3.3 LUT颜色矩阵
1. 要创建合适的LUT纹理,我们需要用颜色转换矩阵填充它。可以通过调整颜色分级的Pass方法来使用从UV坐标提取的颜色,而不是对源纹理进行采样。定义GetColorGradedLUT方法得到颜色值然后立即进行颜色分级,颜色分级的Pass各个片元函数只需要调用色调映射。
float3 GetColorGradedLUT(float2 uv, bool useACES = false)
{
float3 color = float3(uv, 0.0);
return ColorGrade(color, useACES);
}
float4 ColorGradingNonePassFragment (Varyings input) : SV_TARGET
{
float3 color = GetColorGradedLUT(input.screenUV);
return float4(color, 1.0);
}
float4 ColorGradingReinhardPassFragment(Varyings input) : SV_TARGET
{
float3 color = GetColorGradedLUT(input.screenUV);
color /= color + 1.0;
return float4(color, 1.0);
}
float4 ColorGradingNeutralPassFragment(Varyings input) : SV_TARGET
{
float3 color = GetColorGradedLUT(input.screenUV);
color = NeutralTonemap(color);
return float4(color, 1.0);
}
float4 ColorGradingACESPassFragment(Varyings input) : SV_TARGET
{
float3 color = GetColorGradedLUT(input.screenUV, true);
color = AcesTonemap(color);
return float4(color, 1.0);
}
2. 我们可以通过调用GetLutStripValue方法获取LUT的输入颜色,它需要UV坐标和从CPU发送来的颜色分级LUT参数向量。
float4 _ColorGradingLUTParameters;
float3 GetColorGradedLUT (float2 uv, bool useACES = false)
{
float3 color = GetLutStripValue(uv, _ColorGradingLUTParameters);
return ColorGrade(color, useACES);
}
3. 回到PostFXStack脚本的DoColorGradingAndToneMapping方法中,将LUT相关参数发送到GPU。四个参数分别是LUT高度、0.5除以LUT宽度、0.5除以LUT高度和高度除以高度减一。
int colorGradingLUTParametersId = Shader.PropertyToID("_ColorGradingLUTParameters");
void DoColorGradingAndToneMapping(int sourceId)
{
...
buffer.GetTemporaryRT(colorGradingLUTId, lutWidth, lutHeight, 0,FilterMode.Bilinear, RenderTextureFormat.DefaultHDR);
buffer.SetGlobalVector(colorGradingLUTParametersId, new Vector4(lutHeight, 0.5f / lutWidth, 0.5f / lutHeight, lutHeight / (lutHeight - 1f)));
...
}
下图是LUT没有颜色分级,使用Reinhard色调映射的效果。

12.3.4 Log C LUT
1. 我们获得的LUT矩阵位于线性颜色空间中,仅涵盖 0-1的范围。为了支持HDR,我们必须扩大该范围。可以通过将输入颜色转换到Log C空间来实现,这将范围扩大到略低于59。

float3 GetColorGradedLUT(float2 uv, bool useACES = false)
{
float3 color = GetLutStripValue(uv, _ColorGradingLUTParameters);
return ColorGrade(LogCToLinear(color), useACES);
}
下图是Log C颜色使用Reinhard色调映射的效果。

2. 与线性空间相比,Log C为最黑暗的值增加了一点分辨率,它超过了大约0.5的线性值。之后强度迅速上升,矩阵分辨率降低很多。这时需要覆盖HDR值,如果我们不需要这些值则最好保留在线性空间,否则几乎一半的分辨率被浪费。在着色器中添加一个bool值来控制。
float3 GetColorGradedLUT(float2 uv, bool useACES = false)
{
float3 color = GetLutStripValue(uv, _ColorGradingLUTParameters);
return ColorGrade(_ColorGradingLUTInLogC ? LogCToLinear(color) : color, useACES);
}
3. 当在使用HDR且应用了色调映射的情况下启用Log C模式。
int colorGradingLUTInLogId = Shader.PropertyToID("_ColorGradingLUTInLogC");
void DoColorGradingAndToneMapping(int sourceId)
{
...
buffer.SetGlobalFloat(colorGradingLUTInLogId, useHDR && pass != Pass.ColorGradingNone ? 1f : 0f);
Draw(sourceId, colorGradingLUTId, pass);
...
}
4. 因为我们不再依赖渲染的图像,所以不再需要将颜色范围限制在60。它已经受到LUT范围的限制。
float3 ColorGrade (float3 color, bool useACES = false)
{
//color = min(color, 60.0);
...
}
12.3.5 应用LUT
1. 要应用LUT,我们定义一个Final Pass和相关片元函数,它需要做的是获得源纹理颜色,并将其应用到颜色分级LUT中。在单独的ApplyColorGradingLUT方法中进行处理。
float3 ApplyColorGradingLUT(float3 color)
{
return color;
}
float4 FinalPassFragment(Varyings input) : SV_TARGET
{
float4 color = GetSource(input.screenUV);
color.rgb = ApplyColorGradingLUT(color.rgb);
return color;
}
Pass
{
Name "Final"
HLSLPROGRAM
#pragma target 3.5
#pragma vertex DefaultPassVertex
#pragma fragment FinalPassFragment
ENDHLSL
}
2. 可以通过ApplyLut2D方法应用LUT,该功能可将2D LUT切片解释为3D纹理。该方法需要LUT纹理和采样器作为参数,然后是输入颜色,无论是线性还是Log C色彩空间,最后是参数向量的XYZ分量。
TEXTURE2D(_ColorGradingLUT);
float3 ApplyColorGradingLUT(float3 color)
{
return ApplyLut2D(TEXTURE2D_ARGS(_ColorGradingLUT, sampler_linear_clamp),
saturate(_ColorGradingLUTInLogC ? LinearToLogC(color) : color),
_ColorGradingLUTParameters.xyz);
}
3. 将_ColorGradingLUTParameters参数传到GPU,3个分量分别是1除以LUT宽度、1除以LUT高度和LUT高度减1,并使用Final Pass进行最终绘制。
void DoColorGradingAndToneMapping(int sourceId)
{
...
buffer.SetGlobalVector(colorGradingLUTParametersId,new Vector4(1f / lutWidth, 1f / lutHeight, lutHeight - 1f));
Draw(sourceId, BuiltinRenderTextureType.CameraTarget, Pass.Final);
buffer.ReleaseTemporaryRT(colorGradingLUTId);
}
最后,我们是否需要在每帧重新创建LUT纹理呢?
只对LUT纹理进行颜色分级和色调映射比单独渲染图像的所有像素要进行的工作少很多,进一步的优化就是缓存LUT。但是确定是否需要刷新LUT可能会变得复杂,尤其是在支持多相机的不同设置或混合设置时。因此我们坚持每次渲染摄像机时都重新创建LUT,URP和HDRP也是这样做的。
12.3.6 LUT条状带
虽然现在我们使用LUT进行颜色分级和色调映射,但渲染结果应该与以前一样。由于LUT的分辨率有限,我们要使用双线性插值进行采样,因此它将原本平滑的颜色转换为线性带。对于分辨率为32的LUT来说,这通常不明显,但在具有极端HDR颜色变化的区域中,条状带将会变得可见。
下图是LUT分辨率为16和32的对比图,可以看到低分辨率LUT下在颜色过渡比较大时会产生条状带。


通过暂时切换到sampler_point_clamp采样器状态,条状带可以非常明显地观察到,这关闭了LUT 2D切片内部的插值,但相邻切片之间仍有插值,因为ApplyLut2D通过采样两个切片并在它们之间混合来模拟3D纹理。
下面是切换到sampler_point_clamp采样器状态下,LUT分辨率为16和32的效果,如果条状带太明显,可以将LUT分辨率增加到64,但通常稍微变换一下颜色就可以隐藏它。


12|颜色分级
提交
暂无评论